package com.coco.bargain.util;


import com.coco.common.exception.CustomException;
import com.coco.common.exception.ErrorEnum;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * <p>
 *     随机生成砍价金额工具类. 算法逻辑如下：
 *     1. 第一个人砍价支持砍价比率，其他人则根据配置的最小砍价金额与剩余总金额进行随机数生成
 *     （每次生成一个随机金额后，首要重新计算剩余金额，公式为 ： 剩余金额 = 剩余金额 - (剩余人数 * 每人最小砍价金额),保证预留每个人最少的砍价金额）.
 *     2.生成完所有随机数后进行总和校验
 *     3.除第一个人金额外，将其他随机金额随机打乱。目的是： 随着剩余金额越来越小，随机数区间也越来越小。这就导致靠后的随机金额比较均匀不够随机，所以将顺序打乱
 *     注意 ： 当第一个人的砍价金额比率太大时，其他人的随机区间会很小，所以会导致其他人的随机金额趋于平缓，后期会再出一版算法优化
 *               该问题。目前该算法以经过百万人次使用。
 * </p>
 */

@Slf4j
public class RandomBargainAmountUtil {

    public static void main(String[] args) {
        List<String> random = RandomBargainAmountUtil.random(100, 4, 0.96, 0.97, 1);
        random.forEach(System.out::println);
    }


    /**
     * 单位 ：分
     */
    private static final int CENT_MONEY_UNIT = 100;

    /**
     *
     * @param money 砍价任务总金额
     * @param count 砍价任务总人数
     * @param firstPersonCutRateMin 第一个砍价占总金额最小比率
     * @param firstPersonCutRateMax 第一个砍价占总金额最大比率
     * @param minCutPrice 除第一人外，其他人最小砍价金额 （单位：分）
     * @return
     */
    public static List<String> random(final int money, final int count, final double firstPersonCutRateMin,
                                      final double firstPersonCutRateMax, final int minCutPrice) {

        log.info("开始生成随机砍价金额 money:{};count:{};firstPersonCutRateMin:{};firstPersonCutRateMax:{};minCutPrice:{}",
                money, count, firstPersonCutRateMin, firstPersonCutRateMax, minCutPrice);

        if (money <= 0 || minCutPrice <= 0 || count <= 0) {
            log.error("随机金额算法参数校验失败: money:{};minCutPrice:{};count:{}", money, minCutPrice, count);
            throw new CustomException(ErrorEnum.BARGAIN_RANDOM_PARAMETER);
        }

        //保存随机金额
        List<String> list = new ArrayList<>();

        //总金额，单位 ： 元
        int totalMoney = money;

        //将单位： 元  转为 ： 分
        int totalMoneyWithFen = money * CENT_MONEY_UNIT;

        try {

            //计算第一个人的最小、最大金额的区间
            int minPrice = (int) (totalMoneyWithFen * firstPersonCutRateMin);
            int maxPrice = (int) (totalMoneyWithFen * firstPersonCutRateMax);

            //校验随机数量
            if ((count - 1) > ((totalMoneyWithFen - maxPrice) / minCutPrice)) {
                log.error("随机金额算法总金额不足以生成指定随机金额数量 money:{};firstPersonCutRateMin:{};" +
                        "firstPersonCutRateMax:{};minCutPrice:{};count:{}", money, firstPersonCutRateMin, firstPersonCutRateMax, minCutPrice, count);
                throw new CustomException(ErrorEnum.BARGAIN_RANDOM_NUMBER);
            }

            //随机生成第一个人的随机金额
            int remainMoney = randomPrice(totalMoneyWithFen, minPrice, maxPrice, list);

            //生成其他人随机金额
            execute(list, remainMoney, count, minCutPrice);

            //校验 ： 计算生成的总金额是否等于砍价总金额
            if (list.size() != count) {
                log.error("随机生成砍价金额数量与设置不符 list:{};count:{}", list, count);
                throw new CustomException(ErrorEnum.BARGAIN_RANDOM_TOTAL_PRICE);
            }

            BigDecimal sum = BigDecimal.ZERO;
            for (String item : list) {
                sum = sum.add(new BigDecimal(item));
            }

            if (sum.compareTo(new BigDecimal(String.valueOf(totalMoney))) != 0) {
                log.error("随机生成砍价金额与总价不符； list:{};money:{};count:{};minCutPrice:{}", list, money, count, minCutPrice);
                throw new CustomException(ErrorEnum.BARGAIN_RANDOM_TOTAL_PRICE);
            }


            //除第一个的随机金额外，将其他人的随机打乱顺序
            String firstPrice = list.remove(0);
            Collections.shuffle(list);
            list.add(0, firstPrice);

            return list;

        } catch (Exception e) {
            log.error("随机砍价金额生成异常 msg:{}", e.getMessage(), e);
            return null;
        }
    }

    /**
     * 随机生成砍价金额
     * @param list 用于保存生成的砍价金额
     * @param money 砍价总金额 （单位 ： 分）
     * @param count 随机金额数量
     * @param minCutPrice 最小砍价金额
     */
    private static void execute(List<String> list, int money, int count, int minCutPrice) {
        try {
            while (list.size() < count) {

                if (money <= 0) {
                    break;
                }

                //生成随机金额
                money = apply(list, money, count, minCutPrice);

                if (list.size() == count) {
                    break;
                }
            }
        } catch (Exception e) {
            log.error("生成随机砍价金额[RandomBargainAmountUtil#execute]异常 list:{};money:{};count:{};minCutPrice:{}",
                    list, money, count, minCutPrice, e);
            return;
        }
    }

    private static int apply(List<String> list, int money, int count, int minCutPrice) {

        //最后一个人补齐砍价总金额
        if (list.size() == count - 1) {
            list.add(String.valueOf((double) money / CENT_MONEY_UNIT));
            return 0;
        }

        //剩余金额 = 剩余金额 - (剩余人数 * 每人最小砍价金额),保证预留每个人最少的砍价金额
        int remainNum = count - list.size();
        int minRemainPrice = minCutPrice * remainNum;

        //校验剩余金额是否满足剩余人数可以获得最小砍价金额
        if (money < minRemainPrice) {
            log.error("生成随机砍价金额[RandomBargainAmountUtil#execute]异常：剩余金额不满足剩余人数最小金额 " +
                    "list:{};money:{};count:{};minCutPrice:{}", list, money, count, minCutPrice);
            throw new CustomException(ErrorEnum.BARGAIN_RANDOM_MIN_PRICE);
        }

        //校验剩余金额是否等于最小砍价金额，如果是则直接取最小砍价金额
        if (money == minRemainPrice) {
            money -= minCutPrice;
            list.add(String.valueOf((double) minCutPrice / CENT_MONEY_UNIT));
        }

        //校验剩余金额是否大于最小砍价金额，如果是则随机金额
        if (money > minRemainPrice) {
            int maxPrice = money - (minCutPrice * remainNum);
            money = randomPrice(money, minCutPrice, maxPrice, list);
        }

        return money;
    }


    //生成随机数
    private static int randomPrice(int money, int minPrice, int maxPrice, List<String> list) {
        int price = ThreadLocalRandom.current().nextInt(minPrice, maxPrice);
        money -= price;
        list.add(String.valueOf((double) price / CENT_MONEY_UNIT));
        return money;
    }

}